#include "Event.h"
#include "scheduler.h"
#include <algorithm>
#include <cstdio>
#include <ctype.h>
#include <fstream>
#include <getopt.h>
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <vector>

enum Transition {
  TRANS_TO_READY = 1,
  TRANS_TO_PREEMPT = 4,
  TRANS_TO_RUN = 2,
  TRANS_TO_BLOCK = 3,
};

class SimUtils {
private:
  int ofs = 0; // Keep track of random number offset
  int randTotal = 0;
  std::vector<int> rand_vals;

public:
  void prepInputs(char const *rfile, char const *inPath, Des *des,
                  int maxprio) {
    std::ifstream rFile(rfile);

    rFile >> randTotal;
    rand_vals.resize(randTotal);

    for (int i = 0; i < randTotal; ++i) {
      rFile >> rand_vals[i];
    }

    ifstream inFile(inPath);

    int PID = 0;
    while (!inFile.eof()) {
      int AT, TC, CB, IO;
      inFile >> AT >> TC >> CB >> IO;

      if (inFile.fail())
        break; // Exit the loop if reading failed (e.g., due to reaching the end
      // of the file)

      Process *proc_i = new Process;
      proc_i->set_pid(PID++);
      proc_i->set_incrementally(AT);
      proc_i->set_incrementally(TC);
      proc_i->set_incrementally(CB);
      proc_i->set_incrementally(IO);

      Event *e_i = new Event;
      proc_i->set_priority(random_burst(maxprio));
      e_i->set_state(0);
      e_i->set_event_process(proc_i);
      e_i->set_transition(1);
      e_i->set_time_stamp(proc_i->get_arrival_time());
      des->insert_event(e_i);
    }

    inFile.close();
    rFile.close();
  }

  int random_burst(int burst) {
    if (ofs + 1 >= randTotal) {
      ofs = 0;
    }

    int out = 1 + (rand_vals[ofs] % burst);
    ofs++;

    return out;
  }
};

class Simulation {
private:
  std::vector<Process *> done_list;
  Scheduler *THE_SCHEDULER;
  Des des;
  SimUtils utils;
  int quantum;
  int maxprio;
  char schedulerType;
  int CURRENT_TIME = 1;
  Process *CURRENT_RUNNING_PROCESS = nullptr;
  const char *process_path;
  const char *random_path;
  bool CALL_SCHEDULER = false;
  int TOTAL_CPU_TIME = 0;
  int TOTAL_IO_TIME = 0;
  int LAST_BLOCKED = 0;
  int VERBOSE = 0;

public:
  Simulation(const char *proc_path, const char *rand_path, char st, int qtm,
             int mpo, int v) {
    process_path = proc_path;
    random_path = rand_path;
    schedulerType = st;
    quantum = qtm;
    maxprio = mpo;
    VERBOSE = v;
  }

  void select_scheduler() {
    switch (schedulerType) {
    case 'F':
      THE_SCHEDULER = new FCFS;
      break;
    case 'L':
      THE_SCHEDULER = new LCFS;
      break;
    case 'R':
      THE_SCHEDULER = new FCFS;
      break;
    case 'S':
      THE_SCHEDULER = new SRTF;
      break;
    case 'P':
      THE_SCHEDULER = new PRIO;
      break;
    case 'E':
      THE_SCHEDULER = new PREPRIO(maxprio);
    default:
      THE_SCHEDULER = new PRIO;
      break;
    }

    utils.prepInputs(random_path, process_path, &des, maxprio);
  }

  int get_next_event_time() { return (des.peek_event())->get_time_stamp(); }

  void run_simulation() {
    while (des.not_empty()) {
      Event *evt = des.get_event();
      CURRENT_TIME = evt->get_time_stamp();
      Process *proc_e = evt->get_process();
      int last_trans_time = proc_e->last_event_time;
      int TPI = CURRENT_TIME - last_trans_time;
      if (evt->get_state() == 1) {
        proc_e->add_cpu_waiting(TPI);
      }
      proc_e->last_event_time = CURRENT_TIME;

      switch ((Transition)evt->get_transition()) {
      case TRANS_TO_READY: {
        print_transition(evt);
        handle_trans_to_ready(evt);
        break;
      }
      case TRANS_TO_RUN: {
        print_transition(evt);
        handle_trans_to_run(evt);
        break;
      }
      case TRANS_TO_BLOCK: {
        print_transition(evt);
        if ((evt->get_process())->get_time_running() <
            (evt->get_process())->get_total_time()) {
          handle_trans_to_block(evt);
        } else {
          CURRENT_RUNNING_PROCESS = nullptr;
          CALL_SCHEDULER = true;
          (evt->get_process())->set_finishing_time(CURRENT_TIME);
          if (VERBOSE)
            printf("\nprocess %d DONE", evt->get_process()->get_pid());
          done_list.push_back(evt->get_process());
        }
        break;
      }
      case TRANS_TO_PREEMPT: {
        print_transition(evt);
        handle_trans_to_preempt(evt);
        break;
      }
      }

      delete evt;

      if (CALL_SCHEDULER) {
        if (des.not_empty() && (get_next_event_time() == CURRENT_TIME)) {
          continue;
        }

        CALL_SCHEDULER = false;

        if (CURRENT_RUNNING_PROCESS == nullptr) {
          if (!THE_SCHEDULER->empty()) {
            CURRENT_RUNNING_PROCESS = THE_SCHEDULER->get_next_process();
          } else {
            continue;
          }

          Event *run_event = new Event;
          run_event->set_transition(2);
          run_event->set_state(1);
          run_event->set_time_stamp(CURRENT_TIME);
          run_event->set_event_process(CURRENT_RUNNING_PROCESS);
          des.insert_event(run_event);
        }
      }
    }
  }

  void print_transition(Event *evt) {
    Process *proc_i = evt->get_process();
    int pid = proc_i->get_pid();
    Transition transition = (Transition)evt->get_transition();
    int transition_from = evt->get_state();
    std::string trans_to;
    std::string trans_from;
    switch (transition) {
    case TRANS_TO_READY:
      trans_to = "READY";
      break;
    case TRANS_TO_RUN:
      trans_to = "RUN";
      break;
    case TRANS_TO_BLOCK:
      trans_to = "BLOCKED";
      break;
    case TRANS_TO_PREEMPT:
      trans_to = "PREEMPT";
      break;
    }

    switch (transition_from) {
    case 0:
      trans_from = "CREATED";
      break;
    case 1:
      trans_from = "READY";
      break;
    case 2:
      trans_from = "RUNNING";
      break;
    case 3:
      trans_from = "BLOCKED";
      break;
    }

    if (VERBOSE) {
      printf("\n %d %d %s -> %s prio = %d", CURRENT_TIME, pid,
             trans_from.c_str(), trans_to.c_str(), proc_i->dynamic_priority);
    }
  }

  void handle_trans_to_ready(Event *c_event) {
    Process *proc_i = c_event->get_process();

    if (c_event->get_state() == 3)
      proc_i->dynamic_priority = proc_i->get_priority() - 1;

    THE_SCHEDULER->add_process(proc_i);
    CALL_SCHEDULER = true;
  }

  // Create event for either preemption or blocking
  void handle_trans_to_run(Event *c_event) {
    Event *e_i = new Event;
    Process *proc_i = c_event->get_process();
    int cpu_burst_i;

    if (proc_i->cb_remaining == 0) {
      int random_burst_i = utils.random_burst(proc_i->get_cpu_burst());
      cpu_burst_i = std::min(random_burst_i, (proc_i->get_total_time()) -
                                                 (proc_i->get_time_running()));
    } else {
      cpu_burst_i =
          std::min(proc_i->cb_remaining,
                   (proc_i->get_total_time()) - (proc_i->get_time_running()));
    }

    if (VERBOSE == 1) {
      printf(" CB %d rem = %d", cpu_burst_i,
             proc_i->get_total_time() - (proc_i->get_time_running()));
    }
    int quantum_i = quantum;
    int trans;

    // Preempt
    if (cpu_burst_i > quantum_i) {
      proc_i->cb_remaining = std::max(cpu_burst_i - quantum_i, 0);
      proc_i->add_time_running(quantum_i);
      e_i->set_time_stamp(CURRENT_TIME + quantum_i);
      TOTAL_CPU_TIME += quantum_i;
      trans = 4;

    } else {
      proc_i->cb_remaining = std::max(proc_i->cb_remaining - cpu_burst_i, 0);
      proc_i->add_time_running(cpu_burst_i);
      e_i->set_time_stamp(CURRENT_TIME + cpu_burst_i);
      TOTAL_CPU_TIME += cpu_burst_i;
      trans = 3;
    }

    e_i->set_transition(trans);
    e_i->set_state(2);
    e_i->set_event_process(proc_i);
    des.insert_event(e_i);
  }

  void handle_trans_to_block(Event *c_event) {
    CURRENT_RUNNING_PROCESS = nullptr;
    Event *e_i = new Event;
    Process *proc_i = c_event->get_process();
    int io_burst_i = utils.random_burst((proc_i->get_io_burst()));
    proc_i->add_time_blocked(io_burst_i);

    int e_time = CURRENT_TIME + io_burst_i;
    e_i->set_time_stamp(e_time);
    e_i->set_event_process(proc_i);
    e_i->set_transition(1);
    e_i->set_state(3);

    if (e_time > LAST_BLOCKED) {
      if (LAST_BLOCKED > 0) {
        TOTAL_IO_TIME += (e_time - std::max(CURRENT_TIME, LAST_BLOCKED));
      } else {
        TOTAL_IO_TIME += io_burst_i;
      }

      LAST_BLOCKED = e_time;
    }

    if (VERBOSE == 1)
      printf(" IB = %d", io_burst_i);

    des.insert_event(e_i);
    CALL_SCHEDULER = true;
  }

  void handle_trans_to_preempt(Event *c_event) {
    CURRENT_RUNNING_PROCESS = nullptr;
    Process *proc_i = c_event->get_process();
    if ((proc_i->dynamic_priority) > 0) {
      proc_i->dynamic_priority -= 1;
    } else {
      proc_i->dynamic_priority = proc_i->get_priority() - 1;
      proc_i->dynamic_reset = true;
    }

    THE_SCHEDULER->add_process(proc_i);
    CALL_SCHEDULER = true;
  }

  void sort_by_id() {
    std::vector<Process *> temp(done_list.size());

    for (auto it = done_list.begin(); it != done_list.end(); ++it) {
      int position = (*it)->get_pid();
      temp[position] = *it;
    }

    done_list = temp;
  }

  void summarize() {
    sort_by_id();
    int turnaround_count = 0;
    int cpu_waiting_count = 0;
    std::string sched_label;
    bool print_quantum = false;
    if (schedulerType == 'F') {
      sched_label = "FCFS";
    } else if (schedulerType == 'L') {
      sched_label = "LCFS";
    } else if (schedulerType == 'R') {
      sched_label = "RR";
      print_quantum = true;
    } else if (schedulerType == 'S') {
      sched_label = "SJF";
    } else {
      sched_label = "PRIO";
      print_quantum = true;
    }

    if (print_quantum) {
      printf("%s %i\n", sched_label.c_str(), quantum);
    } else {
      printf("%s\n", sched_label.c_str());
    }

    for (auto it = done_list.begin(); it != done_list.end(); ++it) {
      int AT = (*it)->get_arrival_time();
      int TC = (*it)->get_total_time();
      int CB = (*it)->get_cpu_burst();
      int IO = (*it)->get_io_burst();
      int PRIO = (*it)->get_priority();
      int FT = (*it)->get_finishing_time();
      int TT = FT - AT;
      turnaround_count += TT;
      int IT = (*it)->get_time_blocked();
      int CW = (*it)->get_cpu_waiting();
      cpu_waiting_count += CW;
      int pid = (*it)->get_pid();

      printf("%04d: %4d %4d %4d %4d %1d | %5d %5d %5d %5d\n", pid, AT, TC, CB,
             IO, PRIO, FT, TT, IT, CW);
    }

    double CPU_UTIL = ((double)TOTAL_CPU_TIME / (double)CURRENT_TIME) * 100.0;
    double IO_UTIL = ((double)TOTAL_IO_TIME / (double)CURRENT_TIME) * 100.0;

    int n_proc = done_list.size();

    double average_turnaround = (double)turnaround_count / (double)n_proc;
    double average_waiting = (double)cpu_waiting_count / (double)n_proc;
    double throughput = (n_proc / (double)CURRENT_TIME) * 100;

    printf("SUM: %d %.2lf %.2lf %.2lf %.2lf %.3lf\n", CURRENT_TIME, CPU_UTIL,
           IO_UTIL, average_turnaround, average_waiting, throughput);
  }
};

// MAIN
int main(int argc, char **argv) {
  int vflag = 0;
  char *path1 = nullptr;
  char *path2 = nullptr;
  std::string sched;
  int quantum = 1000000;
  int maxprio = 4;
  int c;
  opterr = 0;
  bool traceEvents = false;
  bool showEventQ = false;
  bool showPreemption = false;

  while ((c = getopt(argc, argv, "vtep:s:")) != -1)
    switch (c) {
    case 'v':
      vflag = 1;
      break;
    case 's': {
      sched = std::string(optarg);
      char schedulerType = optarg[0];
      if (schedulerType == 'R' || schedulerType == 'P' ||
          schedulerType == 'E') {
        if (sscanf(optarg + 1, "%d:%d", &quantum, &maxprio) < 1) {
          std::cerr << "Error parsing scheduler specification." << std::endl;
          exit(EXIT_FAILURE);
        }
      }
      break;
    }
    case 't':
      traceEvents = true;
      break;
    case 'e':
      showEventQ = true;
      break;
    case 'p':
      showPreemption = true;
      break;
    default:
      std::cerr << "Usage: " << argv[0]
                << " [-v] [-t] [-e] [-p] [-s<schedspec>] inputfile randfile"
                << std::endl;
      exit(EXIT_FAILURE);
    }

  path1 = argv[optind];
  path2 = argv[optind + 1];

  Simulation simulation(path1, path2, sched[0], quantum, maxprio, vflag);
  simulation.select_scheduler();
  simulation.run_simulation();
  simulation.summarize();

  return 0;
};
